/**
 * Live GPS Tracking Application
 * NMEA Parser
 *
 * Copyright 2009, Ivar Clemens
 */

package nl.ivarclemens.gpstrack.nmea;

import nl.ivarclemens.gpstrack.GPSMessageEvent;
import nl.ivarclemens.gpstrack.nmea.SatelliteEvent;
import nl.ivarclemens.StringTokenizer;

import java.util.Date;
import java.util.Calendar;
import java.util.TimeZone;
import java.util.Vector;
import java.util.Enumeration;

public class NMEA implements GPSMessageEvent {

    private Vector satelliteListener = new Vector();
    private Vector positionListener = new Vector();

    /**
     * Calculates checksum for NMEA message
     */
    public static String getChecksum(String msg)
    {
        int checksum = 0;

        for(int i = 0; i < msg.length(); i++) {
            char ch = msg.charAt(i);

            if(ch == '$') continue;
            if(ch == '*') break;

            checksum ^= ch;
        }

        String hexChecksum = Integer.toHexString(checksum);

        switch(hexChecksum.length()) {
            case 0: return "00";
            case 1: return "0" + hexChecksum;
            case 2: return hexChecksum;
        }
        
        return "00";
    }

    /**
     * Validate NMEA checksum
     */
    public static boolean validateChecksum(String msg)
    {
        String checksum = NMEA.getChecksum(msg);

        try {
            if(msg.substring(msg.indexOf("*") + 1).compareTo(checksum) == 0)
                return true;
        } catch(Exception e) {
        }
        
        return false;
    }

    public void addSatelliteListener(SatelliteEvent listener)
    {
        satelliteListener.addElement(listener);
    }

    public void addPositionListener(PositionEvent listener)
    {
        positionListener.addElement(listener);
    }

    /**
     * Send notification to listeners, such that they can clear
     * their satellite cache before this update round.
     */
    private void notifySatUpdateBegin()
    {
        Enumeration en = satelliteListener.elements();

        while(en.hasMoreElements()) {
            SatelliteEvent obj = (SatelliteEvent) en.nextElement();
            obj.satUpdateBegins();
        }
    }


    /**
     * Send last-message notification to listeners
     */
    private void notifySatUpdateComplete()
    {
        Enumeration en = satelliteListener.elements();

        while(en.hasMoreElements()) {
            SatelliteEvent obj = (SatelliteEvent) en.nextElement();
            obj.satUpdateComplete();
        }
    }

    /**
     * Parses $GPGSV message and notifies listeners about
     *  any updates regarding satellite position and snr.
     */
    private void dispatchGPGSV(String msg)
    {
        StringTokenizer st = new StringTokenizer(msg, ',');

        int totalMsg = Integer.parseInt(st.nextToken());
        int currentMsg = Integer.parseInt(st.nextToken());
        int totalSats  = Integer.parseInt(st.nextToken());

        if(totalSats == 0) {
            this.notifySatUpdateBegin();
            this.notifySatUpdateComplete();
            return;
        }

        // Number of sattelites in this message...
        int msgSats = totalSats - 4 * (currentMsg - 1);

        if(currentMsg == 1)
            this.notifySatUpdateBegin();

        // Read data from message and send update notification
        for(int i = 0; i < msgSats; i++) {
            try {
                String prn = st.nextToken();

                if(prn.compareTo("") == 0) {
                    st.nextToken();
                    st.nextToken();
                    st.nextToken();

                    continue;
                }
                
                int elevation = Integer.parseInt(st.nextToken());
                int azimuth = Integer.parseInt(st.nextToken());
                int snr = 0;

                try {
                    snr = Integer.parseInt(st.nextToken());
                } catch(Exception e) { }

                Enumeration en = satelliteListener.elements();

                while(en.hasMoreElements()) {
                    SatelliteEvent obj = (SatelliteEvent) en.nextElement();
                    obj.satUpdate(Integer.parseInt(prn), azimuth, elevation, snr);
                }
            } catch(Exception e) { }
        }

        if(currentMsg == totalMsg)
            this.notifySatUpdateComplete();
    }

    /**
     * Processes recommended minium message C, which contains
     *  time, position and velocity.
     */
    private void dispatchGPRMC(String msg)
    {
        //061257.000,A,5149.7512,N,00553.5597,E,1.05,0.0,310309,,,A
        // TIME      S,  LAT     L,   LON,    L, S,   A,  Date

        StringTokenizer st = new StringTokenizer(msg, ',');

        String time = st.nextToken();

        // A = Active, V = Invalid
        String status = st.nextToken();
        
        // Ignore invalid messages
        if(status.compareTo("V") == 0)
            return;

        double latitude, longitude, speed, angle;

        // Position...
        latitude = Double.parseDouble(st.nextToken());
        String NS = st.nextToken();
        longitude = Double.parseDouble(st.nextToken());
        String EW = st.nextToken();

        latitude = Math.floor(latitude / 100) + (latitude % 100) / 60;
        longitude = Math.floor(longitude / 100) + (longitude % 100) / 60;

        // Try to parse speed and angle... skip on failure
        try {
            // Originally in knots, converted to km/h
            speed = Double.parseDouble(st.nextToken()) * 1.85200;

            // Direction relative to true north
            angle = Double.parseDouble(st.nextToken());
        } catch(Exception e) {
            speed = 0;
            angle = 0;
        }

        long timestamp;

        // Parse date/time, don't care if it fails...
        try {
            // Date DDMMYY
            String date = st.nextToken();

            Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
 
            // Convert date/time to unix timestamp
            cal.set(Calendar.YEAR, 2000 + Integer.parseInt(date.substring(4, 6)));
            cal.set(Calendar.MONTH, Integer.parseInt(date.substring(2, 4)) - 1);

            int dayOfMonth = Integer.parseInt(date.substring(0, 2));
            cal.set(Calendar.DAY_OF_MONTH, dayOfMonth);

            cal.set(Calendar.HOUR, Integer.parseInt(time.substring(0, 2)));
            cal.set(Calendar.MINUTE, Integer.parseInt(time.substring(2, 4)));
            cal.set(Calendar.SECOND, Integer.parseInt(time.substring(4, 6)));

            Date dateObject;
            dateObject = cal.getTime();
            timestamp = dateObject.getTime() / 1000;
        } catch(Exception e) {
            timestamp = 0;
        }

        // Notify listeners
        Enumeration en = positionListener.elements();

        while(en.hasMoreElements()) {
            PositionEvent obj = (PositionEvent) en.nextElement();
            obj.posUpdate(timestamp, latitude, longitude, angle, speed);
        }
    }

    /**
     * Parses NMEA message and dispatches to listeners
     */
    public void dispatchMsg(String msg)
    {
        // Ignore invalid messages
        //if(!NMEA.validateChecksum(msg) || !(msg.charAt(0) == '$'))
          //  return;

        // Remove checksum
        int checksumIndex = msg.indexOf("*");
        
        // Only strip checksum if present
        if(checksumIndex >= 0)
            msg = msg.substring(0, checksumIndex);

        // Determine message type
        String msgType = "";

        int typeIndex = msg.indexOf(",");

        // First character is $ and CANNOT be the typeIndex
        if(typeIndex <= 0)
            return;

        msgType = msg.substring(1, typeIndex);
        msg = msg.substring(typeIndex + 1);

        if(msgType.compareTo("GPGSV") == 0)
            dispatchGPGSV(msg);

        if(msgType.compareTo("GPRMC") == 0)
            dispatchGPRMC(msg);
    }
}

